package com.gsbelarus.gedemin.lib.sync.protocol;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.util.Log;

import com.gsbelarus.gedemin.lib.sync.protocol.etc.DownloadHelper;
import com.gsbelarus.gedemin.lib.sync.protocol.etc.Parser;
import com.gsbelarus.gedemin.lib.sync.protocol.etc.Updater;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.AddressFaildException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.AuthFaildException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.GUIDFaildException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.IDFaildException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.NotRequiredException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.StopSyncException;
import com.gsbelarus.gedemin.lib.sync.protocol.interfaces.OnChangeProgress;
import com.gsbelarus.gedemin.lib.sync.protocol.interfaces.SyncProtocolModel;
import com.gsbelarus.gedemin.lib.sync.protocol.table.ParsHeaderTable;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.List;

/**
 * Протокол синхронизации.
 * Выполняемые задачи:
 * 1. Организация доступа к серверу.
 * 2. Загрузка актуальных данных в отдельном потоке.
 * 3. Анализ данных пришедших с сервера и обновление БД.
 */
final public class SyncProtocol {

    private Context context;

    /**
     * Статус завершения синхронизации
     * (ID_FAILED, NO_INTERNET_CONNECTION, AUTH_FAILED, ADDRESS_FAILED, TIMEOUT,
     * NOT_REQUIRED, GUID_FAILED, PARS_FAILED, ALSE_FAILED, SUCCESSFUL, SUCCESSFUL_DEMO, STOP_SYNC)
     */
    public enum SyncStatus {

        /**
         * Неверный ID устройства
         */
        ID_FAILED,

        /**
         * Нет подключения к интернет
         */
        NO_INTERNET_CONNECTION,

        /**
         * Ошибка аутентификации
         */
        AUTH_FAILED,

        /**
         * Ошибка адреса сервера, возникает если пришли "левые" данные
         */
        ADDRESS_FAILED,

        /**
         * Таймаут (30 сек)
         */
        TIMEOUT,

        /**
         * Обновление не требуется, возникает если макс версия равна текущей
         */
        NOT_REQUIRED,

        /**
         * Ошибка гуида
         */
        GUID_FAILED,

        /**
         * Ошибка анализа данных, возникает если сервер выключается во время синхронизации или присылает пустой ответ
         */
        PARS_FAILED,

        /**
         * Другие ошибки
         */
        ALSE_FAILED,

        /**
         * Синхронизация завершена успешно
         */
        SUCCESSFUL,

        /**
         * Создание Демо данных прошло успешно
         */
        SUCCESSFUL_DEMO,

        /**
         * Синхронизация остановлена
         */
        STOP_SYNC
    }

    private boolean isWork = false;
    private boolean isWorkMod = true;
    private volatile boolean isStopSync = false;

    private SyncProtocolModel listener;

    private DataBaseModel dbModel;

    /**
     * Констуктор протокола, запуск синхронизации производится методом doSync
     *
     * @param context  контекст приложения
     * @param dbModel  объект класса для доступа к БД
     * @param listener интерфейс взаимодействия между приложением и протоколом синхронизации
     */
    public SyncProtocol(Context context, DataBaseModel dbModel, SyncProtocolModel listener) {
        this.listener = listener;
        this.context = context;
        this.dbModel = dbModel;
    }

    /**
     * Провести синхронизацию.
     *
     * @return true - синхронизация запущена, false - синхронизация уже работает
     */
    public boolean doSync() {
        if (isWork)
            return false;
        else
            new Sync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

        return true;
    }

    /**
     * Проверка на наличие интернет соединения
     *
     * @return есть соединение или нет
     */
    public boolean checkInternetConnection() {
        ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = conMgr.getActiveNetworkInfo();
        if (netInfo == null || !netInfo.isConnected())
            return false;
        return true;
    }

    /**
     * Класс загрузки и обновления БД в отдельном потоке
     */
    private class Sync extends AsyncTask<Void, Integer, SyncStatus> implements OnChangeProgress {

        private int totalBlocks = 1;            // общее количество блоков
        private int currentBlock = 1;           // загружаемый блок

        private int bufBlockPercent = 0;        // буферная переменная для состояния загрузки одного блока
        private float globalPercent = 0;        // переменная для общего состояния загрузки
        private float bufGlobalPercent = 0;     // буферная переменная для общего состояния загрузки

        private static final long SLEEP_INTERVAL_MILLISECONDS = 5 * 60 * 1000;
        private static final int MAX_COUNT = 5;

        /**
         * расчет общего прогресса. blockPercent - прогресс загрузки одного блока
         */
        @Override
        public void onChangeProgress(int blockPercent) {

            /** если буферная переменная стостояния загрузки одного блока больше текущей загрузки,
             *значит блок докачался и можно обнулять буферную переменную */
            if (blockPercent < bufBlockPercent)
                bufBlockPercent = 0;

            /** расчет общего состояния загрузки */
            globalPercent += 100 / (float) totalBlocks / 100 * (float) (blockPercent - bufBlockPercent);

            bufBlockPercent = blockPercent;

            /** публиковать прогресс только если изменения общего состояния загрузки больше чем на 1% */
            if (globalPercent - bufGlobalPercent >= 1) {
                publishProgress((int) globalPercent, currentBlock, totalBlocks);
                bufGlobalPercent = globalPercent;
            }
        }

        /**
         * Выполняется во время подготовки к созданию нового потока загрузки, имеет доступ к UI потоку
         */
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            isWork = true;
            listener.onPreExecute();
            /** Проверка на подключение к сети */
            if (isWorkMod && !checkInternetConnection()) {
                cancel(true);
                onPostExecute(SyncStatus.NO_INTERNET_CONNECTION);
            }
        }

        @Override
        protected SyncStatus doInBackground(Void... voids) {
            SyncStatus status;

            if (isWorkMod) {
                status = SyncStatus.SUCCESSFUL;
                int currentVer = dbModel.getVerData();
                dbModel.beginTransaction();
                DownloadHelper downloadHelper = new DownloadHelper(this);

                try {
                    Parser parser = new Parser();

                    checkIsStopped();

                    /** делаем запрос на получение макс версии*/
                    List<String> maxVer = downloadHelper.download(listener.getUrlStringForMaxVer(), false);

                    /** проверяем GUID*/
                    ParsHeaderTable header = parser.parsingHeader(maxVer);
                    checkGUID(header.getGuid());
                    Log.d("MAX_VER", header.getNum() + "");
                    Log.d("GUID", header.getGuid() + "");

                    /** запускаем цикл запросов на сервер*/
                    if (header.getNum() > currentVer) {
                        totalBlocks = header.getNum() - currentVer;
                        publishProgress(0, currentBlock, totalBlocks);
                        for (int i = currentVer + 1; i <= header.getNum(); i++) {
                            List<String> line = null;
                            /** запуск цикла для повторения запросов в случае timeout exception */
                            for (int count = 0; count < MAX_COUNT; count++) {
                                try {
                                    checkIsStopped();

                                    line = downloadHelper.download(listener.getUrlStringForVer(i), true);
                                    break;
                                } catch (SocketTimeoutException e) {
                                    if (count == MAX_COUNT - 1)
                                        throw new SocketTimeoutException();
                                    Thread.sleep(SLEEP_INTERVAL_MILLISECONDS);
                                }
                            }
                            currentBlock++;

                            /** проверяем GUID*/
                            ParsHeaderTable headerTable = parser.parsingHeader(line);
                            checkGUID(header.getGuid());
                            Log.d("NUM_VER", headerTable.getNum() + "");
                            Log.d("GUID", headerTable.getGuid() + "");

                            /** методо для обновления базы данных */
                            Updater.update(parser.parsing(line), dbModel);
                        }
                    } else {
                        Updater.updateDateSync(dbModel);

                        checkIsStopped();

                        dbModel.transactionSuccessful();
                        throw new NotRequiredException();
                    }

                    checkIsStopped();

                    /** подтверждаем транзакцию */
                    dbModel.transactionSuccessful();

                } catch (AuthFaildException e) {
                    return SyncStatus.AUTH_FAILED;

                } catch (AddressFaildException e) {
                    return SyncStatus.ADDRESS_FAILED;

                } catch (SocketTimeoutException e) {
                    return SyncStatus.TIMEOUT;

                } catch (NotRequiredException e) {
                    return SyncStatus.NOT_REQUIRED;

                } catch (GUIDFaildException e) {
                    return SyncStatus.GUID_FAILED;

                } catch (IDFaildException e) {
                    return SyncStatus.ID_FAILED;

                } catch (IndexOutOfBoundsException e) {
                    /** может произойти если во время синхронизации сервер выключился или прислал пустой ответ.
                     * генерируется в классе Parser */
                    return SyncStatus.PARS_FAILED;

                } catch (StopSyncException e) {
                    return SyncStatus.STOP_SYNC;

                } catch (IOException e) {
                    /** во всех других случаях говорим о сбое*/
                    return SyncStatus.ALSE_FAILED;

                } catch (InterruptedException e) {
                    return SyncStatus.ALSE_FAILED;

                } finally {
                    /** завершаем транзакцию */
                    dbModel.endTransaction();
                }

            } else {
                status = SyncStatus.SUCCESSFUL_DEMO;
                /** создаем демо данные */
                dbModel.beginTransaction();

                dbModel.createDemo();
                Updater.putGUID(context, "demo_database");

                if (isStopSync)
                    status = SyncStatus.STOP_SYNC;
                else
                    dbModel.transactionSuccessful();
                dbModel.endTransaction();
            }
            return status;
        }

        /**
         * Выполняется после загрузки, имеет доступ к UI потоку
         */
        @Override
        protected void onPostExecute(SyncStatus status) {
            super.onPostExecute(status);
            isWork = false;

            if (isStopSync)
                status = SyncStatus.STOP_SYNC;

            switch (status) {
                case GUID_FAILED:
                    /** чистим базу и запускаем синхронизация заново*/
                    dbModel.recreateDataBase();
                    Updater.putGUID(context, null);
                    doSync();
                    break;
                case ID_FAILED:
                    dbModel.recreateDataBase();
                    Updater.putGUID(context, null);
                    break;
            }

            /** обрабатываем ошибки */
            listener.onPostExecute(status);
        }

        /**
         * Обновление состояния загрузки
         */
        @Override
        protected void onProgressUpdate(Integer... values) {
            listener.onProgressUpdate(values[0], values[1], values[2]);
        }
    }

    /**
     * проверка, остановлена синхронизация или нет
     */
    private void checkIsStopped() throws StopSyncException {
        if (isStopSync)
            throw new StopSyncException();
    }

    public void checkGUID(String guid) throws IOException {
        /** если guid-а на мобильном нет - сохраняем его*/
        if (Updater.getGUID(context) == null)
            Updater.putGUID(context, guid);

        /** проверяем guid, если не равен - чистим значение guid и базу данных*/
        else if (!guid.equals(Updater.getGUID(context)))
            throw new GUIDFaildException();
    }

    /**
     * @return работает ли синхронизация или создание демо данных
     */
    public boolean isWork() {
        return isWork;
    }

    /**
     * @return в каком режиме работает
     */
    public boolean isWorkMod() {
        return isWorkMod;
    }

    /**
     * @param isWorkMod флаг отображающий в каком режиме работать
     */
    public void setWorkMod(boolean isWorkMod) {
        this.isWorkMod = isWorkMod;
    }

    /**
     * Прервать синхронизацию
     */
    public void setStopSync(boolean flag) {
        isStopSync = flag;
    }

    public boolean isStopSync() {
        return isStopSync;
    }
}
